/*******************************************************************************
 * Copyright (c) 2007 University of Oslo.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     University of Oslo - initial API and implementation
 *******************************************************************************/

package com.cea.papyrus.diagram.sequence.sedi.edit.command;

import static com.cea.papyrus.diagram.sequence.sedi.model.util.ModelUtil.getOperands;
import static com.cea.papyrus.diagram.sequence.sedi.model.util.ModelUtil.neIsInstance;

import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.commands.Command;
import org.eclipse.uml2.uml.CombinedFragment;
import org.eclipse.uml2.uml.Interaction;
import org.eclipse.uml2.uml.InteractionFragment;
import org.eclipse.uml2.uml.InteractionOperand;
import org.eclipse.uml2.uml.InteractionUse;
import org.eclipse.uml2.uml.NamedElement;

import com.cea.papyrus.diagram.sequence.sedi.model.util.ModelUtil;
import com.cea.papyrus.diagraminterchange2.di2.Diagram;
import com.cea.papyrus.diagraminterchange2.di2.GraphNode;

/**
 * Determines the owner of a specific point in the diagram. The owner will be the nearest
 * InteractionOperand within a CombinedFragment that covers the point, or in the absense
 * of a cfragment, the Interaction itself. Points outside a frame selects the nearest
 * Interaction. In the absense of any interactions, returns null.
 * <p>
 * As an extra case, the command can also determine if the nearest enclosing element is an
 * InteractionUse. To get the result of this analysis, query the result with
 * {@link #getOwner2}.
 * </p>
 * <p>
 * The following attributes must be set before the command can run successfully:
 * </p>
 * <ul>
 * <li>AnyEditPart
 * <li>Location - in absolute coordinates
 * <li>Diagram
 * </ul>
 * <p>
 * When completed, the following attributes can be read to access the result:
 * </p>
 * <ul>
 * <li>Owner - returns the owning Interaction or InteractionOperand
 * <li>Owner2 - returns the exact owner, including InteractionUse if so.
 * </ul>
 * 
 * @author frankd@ifi.uio.no
 */
public class AnalyseOwnerCommand extends Command {

    private EditPart anyEditPart;
    private Point location;
    private Diagram diagram;
    private InteractionFragment owner;

    public void setAnyEditPart(final EditPart anyEditPart) {
        this.anyEditPart = anyEditPart.getRoot();
    }

    public void setDiagram(final Diagram diagram) {
        this.diagram = diagram;
    }

    public void setLocation(final Point location) {
        this.location = location;
    }

    /**
     * Returns the Interaction or InteractionOperand that has been determined as the
     * owner.
     */
    public InteractionFragment getOwner() {
        return (owner instanceof InteractionUse) ? (InteractionFragment) (owner.getOwner()) : owner;
    }

    /**
     * Returns the nearest direct owner - either an Interction, InteractionOperand or
     * InteractionUse (for gates).
     */
    public InteractionFragment getOwner2() {
        return owner;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void execute() {
        NamedElement best = null;
        Rectangle bestBounds = null;

        TreeIterator<EObject> contents = diagram.eAllContents();
        while (contents.hasNext()) {
            Object o = contents.next();
            if (neIsInstance(o, Interaction.class)) {
                GraphNode candidate = (GraphNode) o;
                Rectangle candidateBounds = getAbsoluteBounds(candidate);

                if (isOnBorder(location, candidateBounds)) {
                    owner = ModelUtil.<InteractionFragment> getSemantic(candidate);
                    return;
                }

                if (best == null
                        || distance(candidateBounds, location) < distance(bestBounds, location)) {
                    best = ModelUtil.<NamedElement> getSemantic(candidate);
                    bestBounds = candidateBounds;
                }
            } else if (neIsInstance(o, CombinedFragment.class)) {
                GraphNode candidate = (GraphNode) o;
                Rectangle candidateBounds = getAbsoluteBounds(candidate);

                if (best == null || betterMatch(bestBounds, candidateBounds)) {
                    Object[] result = selectBestOperand(candidate);
                    best = (NamedElement) result[0];
                    bestBounds = (Rectangle) result[1];
                }
            }
            // InteractionUses are not normally owners, except for
            // that they own ActualGates
            else if (neIsInstance(o, InteractionUse.class)) {
                GraphNode candidate = (GraphNode) o;
                Rectangle candidateBounds = getAbsoluteBounds(candidate);

                if (best == null || betterMatch(bestBounds, candidateBounds)) {
                    best = ModelUtil.<NamedElement> getSemantic(candidate);
                    bestBounds = candidateBounds;
                }
            }
        }

        // TODO If best is CF, then select the best InteractionOperand
        owner = (best == null) ? null : (InteractionFragment) best;
    }

    // no need to override undo since the command doesn't write anything.
    // public void undo() {
    // }

    /** Returns the bounds of the given view in absolute coords. */
    @SuppressWarnings("unchecked")
    private Rectangle getAbsoluteBounds(GraphNode view) {
        Rectangle bounds = new Rectangle(view.getPosition(), view.getSize());
        return getAbsoluteBounds(view, bounds.getCopy());
    }

    /**
     * Determines whether the given point is on the border of the given rectangle.
     */
    private boolean isOnBorder(final Point location, final Rectangle rect) {
        return (((location.x == rect.x || location.x == rect.right() - 1) && location.y >= rect.y && location.y < rect
                .bottom()) || ((location.y == rect.y || location.y == rect.bottom() - 1)
                && location.x >= rect.x && location.x < rect.right()));
    }

    /**
     * Returns the given bounds in absolute coords, using the view to calculate from.
     * <p>
     * 'bounds' is translated in-place but it also returned as the result for convenience.
     * </p>
     */
    @SuppressWarnings("unchecked")
    private Rectangle getAbsoluteBounds(GraphNode view, Rectangle bounds) {
        GraphicalEditPart candidateEP =
                (GraphicalEditPart) anyEditPart.getViewer().getEditPartRegistry().get(view);
        candidateEP.getFigure().translateToAbsolute(bounds);
        return bounds;
    }

    /**
     * Selects the operand within the CombinedFragment that contains the target 'location'
     */
    private Object[] selectBestOperand(final GraphNode cfragmentView) {
        CombinedFragment cfragment = ModelUtil.<CombinedFragment> getSemantic(cfragmentView);
        for (InteractionOperand operand : getOperands(cfragment)) {
            Rectangle operandBounds = OperandUtils.getOperandBounds(diagram, operand, anyEditPart);
            operandBounds = getAbsoluteBounds(cfragmentView, operandBounds);
            if (operandBounds.getResized(1, 1).contains(location))
                return new Object[] {
                        operand, operandBounds
                };
        }
        throw new IllegalArgumentException(String.valueOf(location)); // point
        // not
        // within CF
    }

    /**
     * Returns the distance between the center of the rectangle and the point, both are
     * expected to be absolute coordinates.
     */
    @SuppressWarnings("unchecked")
    private int distance(final Rectangle r, final Point p) {
        return p.getDistance2(r.getCenter());
    }

    /**
     * Returns true iff
     * <ul>
     * <li>'candidate' contains the target location (is a match), and
     * <li>'candidate' is contained within 'best' (is a better match)
     * </ul>
     * Both coordinates should be expressed as absolute coordinates.
     */
    @SuppressWarnings("unchecked")
    private boolean betterMatch(final Rectangle best, final Rectangle candidate) {
        return candidate.getResized(1, 1).contains(location)
                && best.contains(candidate.getCenter());
        // We compare with the "center" of the candiadate rather than the
        // top-left corner because CFs can be extra-global
    }

    @Override
    public void redo() {
        execute();
    }

    @Override
    public void dispose() {
        super.dispose();
        location = null;
        diagram = null;
        owner = null;
    }
}
